Has Pattern
gpt-5.icon
Has型クラスパターン(いわゆる Has pattern)は、主に Haskell で使われる設計テクニックで、
「巨大な環境レコード(AppEnv / Config / Context など)に直接依存せず、必要な“部品”だけを型クラス経由で取り出す」
ためのパターンです。
かなり実務寄りの話なので、順番に説明します。
まず素朴な設計から。
よくある構成はこうです:
code:haskell
data Env = Env
{ envLogger :: Logger
, envDB :: DB
, envCache :: Cache
}
foo :: Env -> IO ()
foo env = do
logInfo (envLogger env) "hello"
問題点:
foo は Logger しか使ってないのに Env 全体に依存する
Env が肥大化すると、関数の依存関係が見えなくなる
テスト時に全部モックする羽目になる
ここを改善したい、というのが出発点です。
Hasパターンではこうします。
まず「X を持っている」ことを表す型クラスを定義:
code:haskell
class HasLogger env where
getLogger :: env -> Logger
Env にインスタンスを与える:
code:haskell
instance HasLogger Env where
getLogger = envLogger
関数側はこう書けます:
code:haskell
foo :: HasLogger env => env -> IO ()
foo env =
logInfo (getLogger env) "hello"
ここがポイントです:
foo は Env を知らない
「Logger を持っている何か」だけを要求している
つまり:
データ構造ではなく 能力 に依存する
という形になります。
実際には複数の依存を組み合わせます:
code:haskell
class HasDB env where
getDB :: env -> DB
bar :: (HasLogger env, HasDB env) => env -> IO ()
これで bar は
Logger
DB
だけに依存していることが型で分かります。
さらに mtl / ReaderT と組み合わせるとこうなります:
code:haskell
foo :: (MonadReader env m, HasLogger env, MonadIO m) => m ()
foo = do
logger <- asks getLogger
liftIO $ logInfo logger "hello"
かなり一般的なスタイルです。
なぜこのパターンが使われるか:
1. 依存関係が型に現れる
2. 巨大な Env への密結合を防げる
3. テスト時に小さいダミー環境を作れる
4. 機能単位で責務を分離できる
要するに、DI(Dependency Injection)を型クラスでやっている感じです。
注意点もあります。
型クラスが増えすぎると把握が大変
エラーメッセージが読みにくくなる
IDE 補完が弱くなることがある
なので、
境界層(usecase / handler)あたりで使う
domain ロジックでは使いすぎない
という運用が多いです。